﻿// Copyright (c) 2014-16 Mark Pearce
// http://opensource.org/licenses/MIT

using System;
using System.ComponentModel;
using System.Configuration;
using System.ServiceProcess;
using System.Configuration.Install;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

namespace GrayParrot.ServiceProcess
{
    public abstract class ReliableService<T> : ServiceBase where T : class
    {
        // Control how long to wait for environment to stabilise 
        // before restarting the worker thread after it has crashed.
        // This delay is in milliseconds.
        private const string CONFIG_WORKER_RESTART_DELAY = "WorkerThreadRestartDelay";
        private const Int32 DEFAULT_WORKER_RESTART_DELAY = 3000;

        private const string NUMBER_OF_RUNNING_THREADS = "NumberOfRunningThreads";
        private const Int32 DEFAULT_NUMBER_OF_THREADS = 1;

        // Names for application-level threads.
        private const string THREAD_NAME_CONTROLLER = "Controller";

        // Backing fields for the ServiceStopRequested property.
        private static readonly object LockFlag = new object();
        private bool m_ServiceStopRequested = false;
        Task[] tasks = null;

        public ReliableService()
        {
            this.CanShutdown = true;
            this.CanStop = true;
            this.CanPauseAndContinue = false;
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
        }

        // The controller thread has the following responsibilities:
        // Start the real work, and then restart it after an unhandled exception.
        // Stop the service if requested by the SCM.
        private Thread ThreadController { get; set; }
        private CancellationTokenSource tokenSource = null;

        // Set by an SCM thread when it wants this service 
        // to stop, and read by the controller and work threads.
        // Protected by a lock, which is simpler to understand
        // than using the more fancy concept of a volatile field.
        // http://www.albahari.com/threading/part4.aspx#_The_volatile_keyword
        private bool ServiceStopRequested
        {
            get
            {
                lock (LockFlag)
                {
                    return m_ServiceStopRequested;
                }
            }
            set
            {
                lock (LockFlag)
                {
                    m_ServiceStopRequested = value;
                }
            }
        }

        // Number of milliseconds to delay worker thread restart.
        private Int32 RestartDelay
        {
            get
            {
                Int32 restartDelay;
                if (!Int32.TryParse(ConfigurationManager.AppSettings[CONFIG_WORKER_RESTART_DELAY], out restartDelay))
                {
                    restartDelay = DEFAULT_WORKER_RESTART_DELAY;
                }
                return restartDelay;
            }
        }

        private Int32 NumberOfThreads
        {
            get
            {
                Int32 numberOfThreads;
                if (!Int32.TryParse(ConfigurationManager.AppSettings[NUMBER_OF_RUNNING_THREADS], out numberOfThreads))
                {
                    numberOfThreads = DEFAULT_NUMBER_OF_THREADS;
                }
                GrayParrot.Logging.Logger.For<T>().DebugFormat("Num. Threads: {0}", numberOfThreads);
                return numberOfThreads;
            }
        }

        protected abstract void Worker();
        protected abstract void DisposeWorker();

        // SCM requests service start using its own thread.
        // This method must complete within 10 seconds of it
        // starting. Otherwise the SCM diagnoses a hang.
        protected override void OnStart(string[] args)
        {
            AppDomain currentDomain = AppDomain.CurrentDomain;
            currentDomain.UnhandledException += new UnhandledExceptionEventHandler(this.UnhandledExceptionFilter);

            GrayParrot.Logging.Logger.For<T>().Info("SCM requested service start. ServiceMain.OnStart");

            this.ThreadController = new Thread(new ThreadStart(ControllerThreadRunning));
            this.ThreadController.Name = THREAD_NAME_CONTROLLER;
            this.ThreadController.Start();
            base.OnStart(args);
        }

        // Invoked when the controller thread starts. 
        private void ControllerThreadRunning()
        {
            GrayParrot.Logging.Logger.For<T>().Info("Controller thread has started. ServiceMain.ControllerThreadRunning");

            // And we're on our way.
            Task[] realWork = null;
            while (!this.ServiceStopRequested)
            {
                // Start real work and then block until that finishes or crashes.
                realWork = this.LaunchWorkAsync();
                try
                {
                    Task.WaitAll(realWork, tokenSource.Token);
                }
                catch (AggregateException ae)
                {
                    GrayParrot.Logging.Logger.For<T>().Error("Thread cancel (ae)", ae);
                }
                catch (Exception crap)
                {
                    GrayParrot.Logging.Logger.For<T>().Error("Thread cancel (crap)", crap);
                }

                // If SCM didn't request a service stop, the assumption is that 
                // the real work crashed and needs to be restarted.
                if (!this.ServiceStopRequested)
                {
                    this.PauseControllerThread("Pause before restarting work cycle.", this.RestartDelay);
                }

                string msg = "ControllerThreadRunning. ";
                for (int i = 0; i < realWork.Length; i++)
                {
                    msg += string.Format("Task {0} Status {1}. ", realWork[i].Id, realWork[i].Status);
                }
                GrayParrot.Logging.Logger.For<T>().Debug(msg);

                ClearAllTasks();
            }

            // This service will now stop.
            GrayParrot.Logging.Logger.For<T>().Info("Service stopping at SCM request. ServiceMain.ControllerThreadRunning");

            this.Cleanup();
        }

        // This method handles all ceremony around the real work of this service.
        private Task[] LaunchWorkAsync()
        {
            tasks = new Task[NumberOfThreads];

            try
            {
                // Launch time.
                tokenSource = new CancellationTokenSource();

                string msg = "LaunchWorkAsync. ";
                for (int i = 0; i < NumberOfThreads; i++)
                {
                    tasks[i] = Task.Factory.StartNew(() => this.DoWork(), tokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
                    
                    msg += string.Format("Task {0} Status {1}. ", tasks[i].Id, tasks[i].Status);

                    //Thread.Sleep(1000);
                }
                GrayParrot.Logging.Logger.For<T>().Debug(msg);
            }

            // Report any exception raised during the work cycle.
            catch (Exception ex)
            {
                string msg = string.Format("Work cycle crashed.\n{0}\n{1}\n{2}", ex.GetType().FullName, ex.Message, ex.StackTrace);
                GrayParrot.Logging.Logger.For<T>().Error(msg);
            }

            return tasks;
        }

        // This is where this service's real work is done.
        // The work cycles continuously until it's asked to stop.
        // If the work crashes with an unhandled exception, the 
        // controller thread will restart it after an appropriate delay.
        private void DoWork()
        {
            /* YUOR CODE HERE */
            Worker();
        }

        // Pause for the specified number of milliseconds.
        private void PauseControllerThread(string message, Int32 waitMilliseconds)
        {
            GrayParrot.Logging.Logger.For<T>().Info(string.Format("ServiceMain.PauseControllerThread. {0}", message));

            // This approach is better than Thread.Sleep.
            // http://msmvps.com/blogs/peterritchie/archive/2007/04/26/thread-sleep-is-a-sign-of-a-poorly-designed-program.aspx 
            using (var pauseControllerThread = new ManualResetEventSlim(initialState: false))
            {
                pauseControllerThread.Wait(waitMilliseconds);
            }
        }

        private void ClearAllTasks()
        {
            foreach (Task t in tasks)
            {
                t.Dispose();
            }

            tasks = null;
        }

        // SCM requests service stop using its own thread.
        protected override void OnStop()
        {
            if (tokenSource != null)
            {
                tokenSource.Cancel();
            }

            /* DISPOSE YOUR OBJECT HERE */
            DisposeWorker();

            GrayParrot.Logging.Logger.For<T>().Info("SCM requested service stop. ServiceMain.OnStop");

            this.ServiceStopRequested = true;
            base.OnStop();
        }

        // SCM requests service stop (via machine shutdown) using its own thread.
        protected override void OnShutdown()
        {
            if (tokenSource != null)
            {
                tokenSource.Cancel();
            }

            /* DISPOSE YOUR OBJECT HERE */
            DisposeWorker();

            GrayParrot.Logging.Logger.For<T>().Info("SCM requested service stop due to machine shutdown. ServiceMain.OnShutdown");

            this.ServiceStopRequested = true;
            base.OnShutdown();
        }

        // To log any unhandled exception - normally this should not be triggered.
        private void UnhandledExceptionFilter(object sender, UnhandledExceptionEventArgs e)
        {
            string msg = string.Format("Service crashed!\n{0}\n{1}", e.ExceptionObject.GetType().FullName, e.ExceptionObject.ToString());
            GrayParrot.Logging.Logger.For<T>().Error(msg);
        }

        // Normal exception, for testing service recovery.
        private void ThrowNormalException()
        {
            Int32 test = 0;
            test = test / test;
        }

        // Process corruption exception, for testing service recovery.
        // From CLR v4 upwards, no managed code can run after 
        // this exception.
        private void ThrowAccessViolationException()
        {
            IntPtr ptr = new IntPtr(1000);
            Marshal.StructureToPtr(1000, ptr, true);
        }

        // Cleanup everything.
        private void Cleanup()
        {
            this.ServiceStopRequested = false;
            //this.AppLog.Dispose();
            base.Dispose(true);
        }
    }
}